package contegor.scire.structure.lang;

import static contegor.scire.structure.Constants.ACTION;
import static contegor.scire.structure.Constants.BASE_CORE;
import static contegor.scire.structure.Constants.TO_BE_SER;
import static contegor.scire.structure.Constants.CONCRETE;
import static contegor.scire.structure.Constants.DECIMAL;
import static contegor.scire.structure.Constants.FIRST;
import static contegor.scire.structure.Constants.INTEGER;
import static contegor.scire.structure.Constants.LANG_ACTIONS;
import static contegor.scire.structure.Constants.LANG_CONCEPTS;
import static contegor.scire.structure.Constants.LANG_MEANING_GROUP;
import static contegor.scire.structure.Constants.LANG_SM;
import static contegor.scire.structure.Constants.LANG_WORDS;
import static contegor.scire.structure.Constants.LAST;
import static contegor.scire.structure.Constants.LEARNED_WORDS;
import static contegor.scire.structure.Constants.TO_MEAN;
import static contegor.scire.structure.Constants.NEXT;
import static contegor.scire.structure.Constants.ORDER;
import static contegor.scire.structure.Constants.PREV;
import static contegor.scire.structure.Constants.TIMESTAMP;
import static contegor.scire.structure.Constants.UNKNOWN_ENTITY;

import java.lang.reflect.InvocationTargetException;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutionException;

import contegor.scire.core.Concept;
import contegor.scire.core.DoubleIdFragment;
import contegor.scire.core.FloatIdFragment;
import contegor.scire.core.Id;
import contegor.scire.core.IdFragment;
import contegor.scire.core.Identifiable;
import contegor.scire.core.IntegerIdFragment;
import contegor.scire.core.Link;
import contegor.scire.core.Link.Flow;
import contegor.scire.core.LongIdFragment;
import contegor.scire.core.MissingElementException;
import contegor.scire.core.Primitive;
import contegor.scire.core.ScireException;
import contegor.scire.core.StringIdFragment;
import contegor.scire.core.TimestampIdFragment;
import contegor.scire.core.modifications.ChainCreateLink;
import contegor.scire.core.modifications.CreateConcept;
import contegor.scire.core.queries.And;
import contegor.scire.core.queries.Filter;
import contegor.scire.core.queries.GetConcepts;
import contegor.scire.core.queries.GetConceptsExtensible;
import contegor.scire.core.queries.GetConceptsIn;
import contegor.scire.core.queries.GetLinksFromSource;
import contegor.scire.core.queries.GetTargetsFromLinks;
import contegor.scire.core.queries.IsConnectedWith;
import contegor.scire.core.queries.Query;
import contegor.scire.core.queries.WithFlow;
import contegor.scire.structure.Context;
import contegor.scire.structure.queries.GetAbstractionsOf;

/**
 * This class will receive complete phrases and will analyze them and translate them to {@link Concept}s
 * that represent their meaning.
 * 
 * @author  Héctor Cirbián Pousa
 */
public class WordsAnalyzer {
  public static final Id MEANING_GROUPS_CONTAINER = new Id("user", "meaning", "groups");

  private Context context;

  public WordsAnalyzer(Context context) throws InterruptedException, ExecutionException {
    this.setContext(context);
  }

  public void setContext(Context context) throws InterruptedException, ExecutionException {
    this.context = context;
    // Some concepts must be preloaded.
    this.context.pull(new GetConcepts(BASE_CORE, UNKNOWN_ENTITY.getLastIdFragment()));
  }

  public Context getContext() {
    return context;
  }

  /** Reads a phrase and sets in the {@link Context} the {@link Concept}s corresponding to its words. */
  public List<Concept> read(String phrase, Locale locale) throws InterruptedException, ExecutionException, UnknownWordException {
    // TODO: Fast done and dirty syntax analyzer. Change to a grammar based one.
    List<Concept> concepts = new ArrayList<Concept>();
    NumberFormat  nf  = NumberFormat.getInstance(locale);
    DateFormat    df  = null;
    ParsePosition pos = new ParsePosition(0);
    for (String word: phrase.split("[ .,]")) {
      pos.setIndex(0);
      Number number = nf.parse(word, pos);
      if ((pos.getIndex() == word.length()) && (number != null)) {
        // Number.
        IdFragment idFragment;
        Id isa;
        if (number instanceof Integer) {
          idFragment = new IntegerIdFragment(number.intValue());
          isa = INTEGER;
        }
        else if (number instanceof Long) {
          idFragment = new LongIdFragment(number.longValue());
          isa = INTEGER;
        }
        else if (number instanceof Float) {
          idFragment = new FloatIdFragment(number.floatValue());
          isa = DECIMAL;
        }
        else {
          idFragment = new DoubleIdFragment(number.doubleValue());
          isa = DECIMAL;
        }
        // Create new primitive in context with that value. If the primitive already exists, then it is returned.
        Id idPrimitive = new Id(Id.RAW_ROOT, idFragment);
        context.process(context.newCreateTransientPrimitive(idPrimitive, number)
          .withIt(new ChainCreateLink(Flow.DOWNSTREAM, isa)
            .withIt(new ChainCreateLink(Flow.NONE, TO_BE_SER)))
            .withIt(new ChainCreateLink(Flow.DOWNSTREAM, CONCRETE))).get();
        // Create the 'word' concept who's 'meaning' is the primitive with the fetched value.
        concepts.addAll(context.process(context.newCreateTransientConcept(new Id(LEARNED_WORDS, idFragment))
          .withIt(new ChainCreateLink(Flow.DOWNSTREAM, idPrimitive)
            .withIt(new ChainCreateLink(Flow.NONE, TO_MEAN)))).get());
        continue;
      }
      if (pos.getIndex() > 0) {
        // Perhaps is a date.
        if (df == null) {
          df = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, locale);
          df.setLenient(true);
        }
        pos.setIndex(0);
        Date date = df.parse(word, pos);
        if (date != null) {
          // Create new primitive in context with that value. If the primitive already exists, then it is returned.
          IdFragment idFragment = new TimestampIdFragment(date);
          Id idPrimitive = new Id(Id.RAW_ROOT, idFragment);
          context.process(context.newCreateTransientPrimitive(new Id(Id.RAW_ROOT, idFragment), date)
            .withIt(new ChainCreateLink(Flow.DOWNSTREAM, TIMESTAMP)
               .withIt(new ChainCreateLink(Flow.NONE, TO_BE_SER)))
               .withIt(new ChainCreateLink(Flow.DOWNSTREAM, CONCRETE))).get();
          // Create the 'word' concept who's 'meaning' is the primitive with the fetched value.
          concepts.addAll(context.process(context.newCreateTransientConcept(new Id(LEARNED_WORDS, idFragment))
            .withIt(new ChainCreateLink(Flow.DOWNSTREAM, idPrimitive)
               .withIt(new ChainCreateLink(Flow.NONE, TO_MEAN)))).get());
          continue;
        }
      }
      // Word.
      IdFragment wordId = new StringIdFragment(word);
      List<Concept> result = context.pull(new GetConcepts(LEARNED_WORDS, wordId));
      if ((result != null) && (result.size() > 0))
        concepts.add(result.get(0));
      else {
        result = context.pull(new GetConcepts(LANG_WORDS, wordId));
        if ((result != null) && (result.size() > 0))
          concepts.add(result.get(0));
        else {
          // Unknown word. Let's see if user is trying to define it.
          concepts.addAll(context.process(new CreateConcept(LEARNED_WORDS, wordId)
            .withIt(context.newCreateTransientLink(Flow.DOWNSTREAM, UNKNOWN_ENTITY)
              .withIt(new ChainCreateLink(Flow.NONE, TO_MEAN)))).get());
        }
      }
    }
    return concepts;
  }

  /**
   * Given the word's {@link Concept}s returned by {@link #read(String, Locale)}, this method looks for its
   * meanings and leave them in the context.
   */
  public void understand(List<Concept> words, Locale locale) throws InterruptedException, ExecutionException, ScireException, SecurityException, IllegalArgumentException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    // First bring locale to context.
    this.context.pull(new GetConcepts(new Id(LANG_CONCEPTS, locale.getLanguage())));
    // Then, get in the context all words meaning and its abstractions.
    for (Concept word: words) {
      Query<Link> query = new Filter<Link>(WithFlow.DOWNSTREAM, new Filter<Link>(new IsConnectedWith<Link>(TO_MEAN, WithFlow.NONE), new GetLinksFromSource(word.getId())));
      for (Link link: context.process(query).get()) {
        Id targetId = link.getTarget();
        this.context.pull(new GetConcepts(targetId));
        this.context.pull(new GetAbstractionsOf(targetId));
      }
    }
    // Add FIRST, NEXT and LAST links.
    for (int i=0; i<words.size(); i++) {
      Concept word = words.get(i);
      Id wordId = word.getId();
      if (i == 0) { // First
        context.process(new GetConceptsExtensible<Link>(wordId.getParentId(), wordId.getLastIdFragment())
            .withIt(context.newCreateTransientLink(Flow.DOWNSTREAM, FIRST))
            .withIt(context.newCreateTransientLink(Flow.DOWNSTREAM, words.get(i+1).getId())
                .withIt(context.newCreateTransientLink(Flow.NONE, NEXT)))).get();
      }
      else if (i == (words.size()-1)) { // Last
        context.process(new GetConceptsExtensible<Link>(wordId.getParentId(), wordId.getLastIdFragment())
            .withIt(context.newCreateTransientLink(Flow.DOWNSTREAM, LAST))
            .withIt(context.newCreateTransientLink(Flow.DOWNSTREAM, words.get(i-1).getId())
                .withIt(context.newCreateTransientLink(Flow.NONE, PREV)))).get();
      }
      else { // Midle
        context.process(new GetConceptsExtensible<Link>(wordId.getParentId(), wordId.getLastIdFragment())
            .withIt(context.newCreateTransientLink(Flow.DOWNSTREAM, words.get(i+1).getId())
                .withIt(context.newCreateTransientLink(Flow.NONE, NEXT)))
            .withIt(context.newCreateTransientLink(Flow.DOWNSTREAM, words.get(i-1).getId())
                .withIt(context.newCreateTransientLink(Flow.NONE, PREV)))).get();         
      }
    }
    // Last, leave best suited to context meanings.
    boolean meaningErased;
    do {
      meaningErased = false;
      for (Concept word: words) {
        // Only 'mean' links.
        Query<Link> query = new Filter<Link>(WithFlow.DOWNSTREAM, new Filter<Link>(new IsConnectedWith<Link>(TO_MEAN, WithFlow.NONE), new GetLinksFromSource(word.getId())));
        for (Link link: context.process(query).get()) {
          // TODO: Add weight filtering, so more complex meanings, if succeed, have more value and are left.
          if (!filterContextComplies(link)) {
            // This 'meaning' is not in sync with the context. So it's erased.
            // TODO: Improve erasing all the "meaning column" (checking other "columns" don't use the same concepts).
            context.process(context.newDeleteConnectable(word.getId(), link.getTarget())).get();
            meaningErased = true;
          }
        }
      }
    }
    while (meaningErased); // Repeat until no 'meaning' links are out of sync.
  }

  /**
   * Filters {@link Link}s and lets it pass if the this {@link Context} complies with the {@link Link}s
   * 'context conditions'. This {@link Link}s are the ones that express lang.words and learned.words definitions.
   * This definitions consist on {@link Link}s pointing to 'first', 'last', current language or other
   * {@link Concept}s (other words or its meaning) with further conditions as 'next' or 'previous'. 
   */
  private boolean filterContextComplies(Link meansLink) throws InterruptedException, ExecutionException {
    for (Link conditionLink: meansLink.getLinks().values()) {
      if (conditionLink.getFlow() == Flow.DOWNSTREAM) { // Other links are meaningful to other analyze algorithms.
        List<Concept> aux = this.context.process(new GetConcepts(conditionLink.getTarget())).get();
        if ((aux == null) || (aux.size() == 0))
          return false;
        Concept conditionTarget = aux.get(0);
        for (Link extraLink: conditionLink.getLinks().values()) {
          aux = this.context.process(new GetTargetsFromLinks(new Filter<Link>(new IsConnectedWith<Link>(extraLink.getTarget(), WithFlow.NONE), new GetLinksFromSource(((Identifiable)meansLink.getSource()).getId())))).get();
          if ((aux == null) || (aux.size() == 0))
            return false;
          for (Concept instance: aux) {
            if (!instanceIsTarget(instance, conditionTarget))
              return false;
          }
        }
      }
    }
    return true;
  }

  private boolean instanceIsTarget(Concept instance, Concept target) throws InterruptedException, ExecutionException {
    if (instance.equals(target))
      return true;
    List<Concept> abstractions = this.context.process(new GetAbstractionsOf(instance.getId())).get();
    if ((abstractions == null) || (abstractions.size() == 0))
      return false;
    for (Concept abstraction: abstractions) {
      if (abstraction.equals(target))
        return true;
    }
    return false;
  }

  /**
   * With the phrase meanings left in the {@link Context}, this method groups them.
   * Transient {@link Primitive}s are used for this grouping, so each group will have one pointing to
   * the members of the meaning group. This {@link Primitive} will also be qualified as other concepts,
   * so it can be known if its function is an action, subject, direct object, etc.
   * 
   * Each {@link Primitive} will have, as value, a {@link MeaningGroupData} that represents its meaning. 
   */
  public List<Primitive> group(List<Concept> words, Locale locale) throws InterruptedException, ExecutionException, UnderstandException, MissingElementException, SecurityException, IllegalArgumentException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    // Bring to context language state machine to group meaning elements.
    Id smContainerId = new Id(LANG_SM, locale.getLanguage());
    context.pull(new GetConceptsIn(smContainerId));

    List<Primitive>  groups          = new ArrayList<Primitive>();
    Primitive        currentGroup    = null;
    MeaningGroupData currentData     = null;
    int              currentCount    = 0;
    Concept          smStart         = context.process(new GetConcepts(new Id(smContainerId, "start"))).get().get(0);
    Concept          smState         = smStart;
    Concept          previousConcept = null;
    int              conceptCount    = 0;
    for (Concept word: words) {
      List<Link> links = context.process(new Filter<Link>(WithFlow.DOWNSTREAM, new Filter<Link>(new IsConnectedWith<Link>(TO_MEAN), new GetLinksFromSource(word.getId())))).get();
      // The word can have more than one meaning, for example Spanish 'del'. So there must be some order in this ...
      if (links.size() > 1)
        Collections.sort(links, new OrderedLinksComparator());
      for (Link link: links) {
        Id targetId = link.getTarget();
        List<Concept> concepts = context.process(new GetConcepts(targetId)).get();
        Concept current = concepts.get(0);
        if (current != previousConcept) {
          previousConcept = current;
          // Repeat until a valid transition is found, or it's impossible to find one ...
          do {
            // Try to find transition ...
            List<Concept> aux = context.process(new GetAbstractionsOf(targetId)).get();
            List<Concept> meaningChain = new ArrayList<Concept>((aux!=null)? aux.size()+1 : 1);
            meaningChain.add(current);
            if (aux != null)
              meaningChain.addAll(aux);
            Link transition = null;
            for (Concept meaning: meaningChain) {
              @SuppressWarnings("unchecked")
              List<Link> transitions = context.process(new Filter<Link>(new And<Link>(WithFlow.DOWNSTREAM, new IsConnectedWith<Link>(meaning.getId(), WithFlow.NONE)), new GetLinksFromSource(smState.getId()))).get();
              if ((transitions == null) || (transitions.size() == 0))
                continue;
              if (transitions.size() > 1)
                throw new UnderstandException("Error in language state machine. Two transitions found for the same meaning: " + smState.getId() + ":" + meaning.getId());
              transition = transitions.get(0);
              break;
            }
  
            if (transition != null) {
              // Create meaning group if it doesn't exists yet.
              if (currentGroup == null) {
                currentData  = new MeaningGroupData();
                currentGroup = context.process(context.newCreateTransientPrimitive(new Id(MEANING_GROUPS_CONTAINER, currentCount), currentData)
                    .withIt(context.newCreateTransientLink(Flow.DOWNSTREAM, LANG_MEANING_GROUP)
                        .withIt(context.newCreateTransientLink(Flow.NONE, TO_BE_SER)))
                        .withIt(context.newCreateTransientLink(Flow.DOWNSTREAM, CONCRETE))).get().get(0);
                conceptCount = 0;
                currentCount++;
                groups.add(currentGroup);
              }
              // Add current Concept to current Meaning Group.
              Link currentLink = context.process(context.newCreateTransientLink(Flow.DOWNSTREAM, currentGroup.getId().getParentId(), current.getId(), currentGroup.getId().getLastIdFragment())).get().get(0);
              context.process(context.newCreateTransientLink(Flow.DOWNSTREAM, currentGroup.getId().getParentId(), ORDER, currentGroup.getId().getLastIdFragment(), current.getId())).get();
              context.process(context.newCreateTransientLink(Flow.DOWNSTREAM, currentGroup.getId().getParentId(), new Id(Id.RAW_ROOT, new IntegerIdFragment(conceptCount)), currentGroup.getId().getLastIdFragment(), current.getId(), ORDER)).get();
              conceptCount++;
              // Execute transition ...
              smState = context.process(new GetConcepts(transition.getTarget())).get().get(0);
              // ... add word meaning (downstream links from word-concept link) to meaningLink's target. Explanation:
              // Given 'link' as the word->concept one, add 'meaningLink' output links to 'currentLink' group->concept. This way they will be available when the language processing logic analyzes the groups and its concepts.
              for (Link meaningLink: link.getLinks().values())
                if (Flow.DOWNSTREAM.equals(meaningLink.getFlow()))
                  context.process(context.newCreateTransientLink(Flow.DOWNSTREAM, currentGroup.getId().getParentId(), meaningLink.getTarget(), currentGroup.getId().getLastIdFragment(), currentLink.getTarget())).get();
              // ... & its action if it has one. This transition's action can add to the group 'be_ser' link more meaning as 'is subject' or 'is direct object', etc.
              for (Link toAction: transition.getLinks().values()) {
                if (toAction.isLinkedWith(ACTION) && Flow.UPSTREAM.equals(toAction.getFlow())) {
                  ActionExecutor executor = new ActionExecutor(this.context);
                  executor.execute(toAction.getTarget(), currentGroup, currentData, link, currentLink);
                }
              }
              // Transition found, not necesary to iterate again ...
              break;
            }
            else {
              // Transition not found. This means current group has 'ended'.
              // So we restart the state machine and current group and the 'find transition' loop is
              // executed again. This way current concept can be incorporated to its group.
              if (currentGroup == null)
                throw new UnderstandException("State machine couldn't find initial transition for: " + current.getId());
              currentGroup = null;
              smState = smStart;
            }
          } while (true);
        }
      }
    }
    return groups;
  }

  /**
   * With the phrase meaning elements grouped, this method process the sentence calling the language specific method.
   *  
   * @throws InvocationTargetException 
   * @throws IllegalAccessException 
   * @throws NoSuchMethodException 
   * @throws ClassNotFoundException 
   * @throws ExecutionException 
   * @throws InterruptedException 
   * @throws IllegalArgumentException 
   * @throws SecurityException 
   * @throws MissingElementException
   */
  public void process(List<Primitive> groups, Locale locale) throws Throwable {
    try {
      ActionExecutor executor = new ActionExecutor(this.context);
      executor.execute(new Id(LANG_ACTIONS, locale.getLanguage()+"Understand"), MEANING_GROUPS_CONTAINER, groups);
    }
    catch (InvocationTargetException ex) {
      throw ex.getCause();
    }
  }
}